﻿#region License
// Copyright (c) 2007 James Newton-King
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using DME.Json.Utilities;
using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.ComponentModel;
using System.Collections.Specialized;
#if !SILVERLIGHT
using DME.Json.Linq.ComponentModel;
#endif

namespace DME.Json.Linq
{
    /// <summary>
    /// Represents a token that can contain other tokens.
    /// </summary>
    public abstract class JContainer : JToken, IList<JToken>
#if !SILVERLIGHT
, ITypedList, IBindingList
#else
    , IList, INotifyCollectionChanged
#endif
    {
#if !SILVERLIGHT
        /// <summary>
        /// Occurs when the list changes or an item in the list changes.
        /// </summary>
        public event ListChangedEventHandler ListChanged;

        /// <summary>
        /// Occurs before an item is added to the collection.
        /// </summary>
        public event AddingNewEventHandler AddingNew;
#else
    /// <summary>
    /// Occurs when the items list of the collection has changed, or the collection is reset.
    /// </summary>
    public event NotifyCollectionChangedEventHandler CollectionChanged;
#endif

        private JToken _content;
        private object _syncRoot;
        private bool _busy;

        internal JToken Content
        {
            get { return _content; }
            set { _content = value; }
        }

        internal JContainer()
        {
        }

        internal JContainer(JContainer other)
        {
            ValidationUtils.ArgumentNotNull(other, "c");

            JToken content = other.Last;
            if (content != null)
            {
                do
                {
                    content = content._next;
                    Add(content.CloneToken());
                }
                while (content != other.Last);
            }
        }

        internal void CheckReentrancy()
        {
            if (_busy)
                throw new InvalidOperationException("Cannot change {0} during a collection change event.".FormatWith(CultureInfo.InvariantCulture, GetType()));
        }

#if !SILVERLIGHT
        /// <summary>
        /// Raises the <see cref="AddingNew"/> event.
        /// </summary>
        /// <param name="e">The <see cref="AddingNewEventArgs"/> instance containing the event data.</param>
        protected virtual void OnAddingNew(AddingNewEventArgs e)
        {
            AddingNewEventHandler handler = AddingNew;
            if (handler != null)
                handler(this, e);
        }

        /// <summary>
        /// Raises the <see cref="ListChanged"/> event.
        /// </summary>
        /// <param name="e">The <see cref="ListChangedEventArgs"/> instance containing the event data.</param>
        protected virtual void OnListChanged(ListChangedEventArgs e)
        {
            ListChangedEventHandler handler = ListChanged;

            if (handler != null)
            {
                _busy = true;
                try
                {
                    handler(this, e);
                }
                finally
                {
                    _busy = false;
                }
            }
        }
#else
    /// <summary>
    /// Raises the <see cref="CollectionChanged"/> event.
    /// </summary>
    /// <param name="e">The <see cref="NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
    protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
      NotifyCollectionChangedEventHandler handler = CollectionChanged;

      if (handler != null)
      {
        _busy = true;
        try
        {
          handler(this, e);
        }
        finally
        {
          _busy = false;
        }
      }
    }
#endif

        /// <summary>
        /// Gets a value indicating whether this token has childen tokens.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this token has child values; otherwise, <c>false</c>.
        /// </value>
        public override bool HasValues
        {
            get { return (_content != null); }
        }

        internal bool ContentsEqual(JContainer container)
        {
            JToken t1 = First;
            JToken t2 = container.First;

            if (t1 == t2)
                return true;

            do
            {
                if (t1 == null && t2 == null)
                    return true;

                if (t1 != null && t2 != null && t1.DeepEquals(t2))
                {
                    t1 = (t1 != Last) ? t1.Next : null;
                    t2 = (t2 != container.Last) ? t2.Next : null;
                }
                else
                {
                    return false;
                }
            }
            while (true);
        }

        /// <summary>
        /// Get the first child token of this token.
        /// </summary>
        /// <value>
        /// A <see cref="JToken"/> containing the first child token of the <see cref="JToken"/>.
        /// </value>
        public override JToken First
        {
            get
            {
                if (Last == null)
                    return null;

                return Last._next;
            }
        }

        /// <summary>
        /// Get the last child token of this token.
        /// </summary>
        /// <value>
        /// A <see cref="JToken"/> containing the last child token of the <see cref="JToken"/>.
        /// </value>
        public override JToken Last
        {
            [DebuggerStepThrough]
            get { return _content; }
        }

        /// <summary>
        /// Returns a collection of the child tokens of this token, in document order.
        /// </summary>
        /// <returns>
        /// An <see cref="IEnumerable{T}"/> of <see cref="JToken"/> containing the child tokens of this <see cref="JToken"/>, in document order.
        /// </returns>
        public override JEnumerable<JToken> Children()
        {
            return new JEnumerable<JToken>(ChildrenInternal());
        }

        internal IEnumerable<JToken> ChildrenInternal()
        {
            JToken first = First;
            JToken current = first;
            if (current == null)
                yield break;

            do
            {
                yield return current;
            }
            while ((current = current.Next) != null);
        }

        /// <summary>
        /// Returns a collection of the child values of this token, in document order.
        /// </summary>
        /// <typeparam name="T">The type to convert the values to.</typeparam>
        /// <returns>
        /// A <see cref="IEnumerable{T}"/> containing the child values of this <see cref="JToken"/>, in document order.
        /// </returns>
        public override IEnumerable<T> Values<T>()
        {
            return Children().Convert<JToken, T>();
        }

        /// <summary>
        /// Returns a collection of the descendant tokens for this token in document order.
        /// </summary>
        /// <returns>An <see cref="IEnumerable{JToken}"/> containing the descendant tokens of the <see cref="JToken"/>.</returns>
        public IEnumerable<JToken> Descendants()
        {
            foreach (JToken o in Children())
            {
                yield return o;
                JContainer c = o as JContainer;
                if (c != null)
                {
                    foreach (JToken d in c.Descendants())
                    {
                        yield return d;
                    }
                }
            }
        }

        internal bool IsMultiContent(object content)
        {
            return (content is IEnumerable && !(content is string) && !(content is JToken) && !(content is byte[]));
        }

        internal virtual void AddItem(bool isLast, JToken previous, JToken item)
        {
            CheckReentrancy();

            ValidateToken(item, null);

            item = EnsureParentToken(item);

            JToken next = (previous != null) ? previous._next : item;

            item.Parent = this;
            item.Next = next;

            if (previous != null)
                previous.Next = item;

            if (isLast || previous == null)
                _content = item;

#if !SILVERLIGHT
            if (ListChanged != null)
                OnListChanged(new ListChangedEventArgs(ListChangedType.ItemAdded, IndexOfItem(item)));
#else
      if (CollectionChanged != null)
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, IndexOfItem(item)));
#endif
        }

        internal JToken EnsureParentToken(JToken item)
        {
            if (item.Parent != null)
            {
                item = item.CloneToken();
            }
            else
            {
                // check whether attempting to add a token to itself
                JContainer parent = this;
                while (parent.Parent != null)
                {
                    parent = parent.Parent;
                }
                if (item == parent)
                {
                    item = item.CloneToken();
                }
            }
            return item;
        }

        internal void AddInternal(bool isLast, JToken previous, object content)
        {
            if (IsMultiContent(content))
            {
                IEnumerable enumerable = (IEnumerable)content;

                JToken multiPrevious = previous;
                foreach (object c in enumerable)
                {
                    AddInternal(isLast, multiPrevious, c);
                    multiPrevious = (multiPrevious != null) ? multiPrevious._next : Last;
                }
            }
            else
            {
                JToken item = CreateFromContent(content);

                AddItem(isLast, previous, item);
            }
        }

        internal int IndexOfItem(JToken item)
        {
            int index = 0;
            foreach (JToken token in Children())
            {
                if (token == item)
                    return index;

                index++;
            }

            return -1;
        }

        internal virtual void InsertItem(int index, JToken item)
        {
            if (index == 0)
            {
                AddFirst(item);
            }
            else
            {
                JToken token = GetItem(index);
                AddInternal(false, token.Previous, item);
            }
        }

        internal virtual void RemoveItemAt(int index)
        {
            if (index < 0)
                throw new ArgumentOutOfRangeException("index", "index is less than 0.");

            CheckReentrancy();

            int currentIndex = 0;
            foreach (JToken token in Children())
            {
                if (index == currentIndex)
                {
                    token.Remove();

#if !SILVERLIGHT
                    OnListChanged(new ListChangedEventArgs(ListChangedType.ItemDeleted, index));
#else
          OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, token, index));
#endif

                    return;
                }

                currentIndex++;
            }

            throw new ArgumentOutOfRangeException("index", "index is equal to or greater than Count.");
        }

        internal virtual bool RemoveItem(JToken item)
        {
            if (item == null || item.Parent != this)
                return false;

            CheckReentrancy();

            JToken content = _content;

            int itemIndex = 0;
            while (content._next != item)
            {
                itemIndex++;
                content = content._next;
            }
            if (content == item)
            {
                // token is containers last child
                _content = null;
            }
            else
            {
                if (_content == item)
                {
                    _content = content;
                }
                content._next = item._next;
            }
            item.Parent = null;
            item.Next = null;

#if !SILVERLIGHT
            OnListChanged(new ListChangedEventArgs(ListChangedType.ItemDeleted, itemIndex));
#else
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, itemIndex));
#endif

            return true;
        }

        internal virtual JToken GetItem(int index)
        {
            return Children().ElementAt(index);
        }

        internal virtual void SetItem(int index, JToken item)
        {
            CheckReentrancy();

            JToken token = GetItem(index);
            token.Replace(item);

#if !SILVERLIGHT
            OnListChanged(new ListChangedEventArgs(ListChangedType.ItemChanged, index));
#else
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, token, index));
#endif
        }

        internal virtual void ClearItems()
        {
            CheckReentrancy();

            while (_content != null)
            {
                JToken o = _content;

                JToken next = o._next;
                if (o != _content || next != o._next)
                    throw new InvalidOperationException("This operation was corrupted by external code.");

                if (next != o)
                    o._next = next._next;
                else
                    _content = null;

                next.Parent = null;
                next._next = null;
            }

#if !SILVERLIGHT
            OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
#else
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
#endif
        }

        internal virtual void ReplaceItem(JToken existing, JToken replacement)
        {
            if (existing == null || existing.Parent != this)
                return;

            if (IsTokenUnchanged(existing, replacement))
                return;

            CheckReentrancy();

            replacement = EnsureParentToken(replacement);

            ValidateToken(replacement, existing);

            JToken content = _content;

            int itemIndex = 0;
            while (content._next != existing)
            {
                itemIndex++;
                content = content._next;
            }

            if (content == existing)
            {
                // token is containers last child
                _content = replacement;
                replacement._next = replacement;
            }
            else
            {
                if (_content == existing)
                {
                    _content = replacement;
                }
                content._next = replacement;
                replacement._next = existing._next;
            }

            replacement.Parent = this;

            existing.Parent = null;
            existing.Next = null;

#if !SILVERLIGHT
            OnListChanged(new ListChangedEventArgs(ListChangedType.ItemChanged, itemIndex));
#else
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, replacement, existing, itemIndex));
#endif
        }

        internal virtual bool ContainsItem(JToken item)
        {
            return (IndexOfItem(item) != -1);
        }

        internal virtual void CopyItemsTo(Array array, int arrayIndex)
        {
            if (array == null)
                throw new ArgumentNullException("array");
            if (arrayIndex < 0)
                throw new ArgumentOutOfRangeException("arrayIndex", "arrayIndex is less than 0.");
            if (arrayIndex >= array.Length)
                throw new ArgumentException("arrayIndex is equal to or greater than the length of array.");
            if (CountItems() > array.Length - arrayIndex)
                throw new ArgumentException("The number of elements in the source JObject is greater than the available space from arrayIndex to the end of the destination array.");

            int index = 0;
            foreach (JToken token in Children())
            {
                array.SetValue(token, arrayIndex + index);
                index++;
            }
        }

        internal virtual int CountItems()
        {
            return Children().Count();
        }

        internal static bool IsTokenUnchanged(JToken currentValue, JToken newValue)
        {
            JValue v1 = currentValue as JValue;
            if (v1 != null)
            {
                // null will get turned into a JValue of type null
                if (v1.Type == JTokenType.Null && newValue == null)
                    return true;

                return v1.Equals(newValue);
            }

            return false;
        }

        internal virtual void ValidateToken(JToken o, JToken existing)
        {
            ValidationUtils.ArgumentNotNull(o, "o");

            if (o.Type == JTokenType.Property)
                throw new ArgumentException("Can not add {0} to {1}.".FormatWith(CultureInfo.InvariantCulture, o.GetType(), GetType()));
        }

        /// <summary>
        /// Adds the specified content as children of this <see cref="JToken"/>.
        /// </summary>
        /// <param name="content">The content to be added.</param>
        public void Add(object content)
        {
            AddInternal(true, Last, content);
        }

        /// <summary>
        /// Adds the specified content as the first children of this <see cref="JToken"/>.
        /// </summary>
        /// <param name="content">The content to be added.</param>
        public void AddFirst(object content)
        {
            AddInternal(false, Last, content);
        }

        internal JToken CreateFromContent(object content)
        {
            if (content is JToken)
                return (JToken)content;

            return new JValue(content);
        }

        /// <summary>
        /// Creates an <see cref="JsonWriter"/> that can be used to add tokens to the <see cref="JToken"/>.
        /// </summary>
        /// <returns>An <see cref="JsonWriter"/> that is ready to have content written to it.</returns>
        public JsonWriter CreateWriter()
        {
            return new JTokenWriter(this);
        }

        /// <summary>
        /// Replaces the children nodes of this token with the specified content.
        /// </summary>
        /// <param name="content">The content.</param>
        public void ReplaceAll(object content)
        {
            ClearItems();
            Add(content);
        }

        /// <summary>
        /// Removes the child nodes from this token.
        /// </summary>
        public void RemoveAll()
        {
            ClearItems();
        }

        internal void ReadContentFrom(JsonReader r)
        {
            ValidationUtils.ArgumentNotNull(r, "r");
            IJsonLineInfo lineInfo = r as IJsonLineInfo;

            JContainer parent = this;

            do
            {
                if (parent is JProperty && ((JProperty)parent).Value != null)
                {
                    if (parent == this)
                        return;

                    parent = parent.Parent;
                }

                switch (r.TokenType)
                {
                    case JsonToken.None:
                        // new reader. move to actual content
                        break;
                    case JsonToken.StartArray:
                        JArray a = new JArray();
                        a.SetLineInfo(lineInfo);
                        parent.Add(a);
                        parent = a;
                        break;

                    case JsonToken.EndArray:
                        if (parent == this)
                            return;

                        parent = parent.Parent;
                        break;
                    case JsonToken.StartObject:
                        JObject o = new JObject();
                        o.SetLineInfo(lineInfo);
                        parent.Add(o);
                        parent = o;
                        break;
                    case JsonToken.EndObject:
                        if (parent == this)
                            return;

                        parent = parent.Parent;
                        break;
                    case JsonToken.StartConstructor:
                        JConstructor constructor = new JConstructor(r.Value.ToString());
                        constructor.SetLineInfo(constructor);
                        parent.Add(constructor);
                        parent = constructor;
                        break;
                    case JsonToken.EndConstructor:
                        if (parent == this)
                            return;

                        parent = parent.Parent;
                        break;
                    case JsonToken.String:
                    case JsonToken.Integer:
                    case JsonToken.Float:
                    case JsonToken.Date:
                    case JsonToken.Boolean:
                    case JsonToken.Bytes:
                        JValue v = new JValue(r.Value);
                        v.SetLineInfo(lineInfo);
                        parent.Add(v);
                        break;
                    case JsonToken.Comment:
                        v = JValue.CreateComment(r.Value.ToString());
                        v.SetLineInfo(lineInfo);
                        parent.Add(v);
                        break;
                    case JsonToken.Null:
                        v = new JValue(null, JTokenType.Null);
                        v.SetLineInfo(lineInfo);
                        parent.Add(v);
                        break;
                    case JsonToken.Undefined:
                        v = new JValue(null, JTokenType.Undefined);
                        v.SetLineInfo(lineInfo);
                        parent.Add(v);
                        break;
                    case JsonToken.PropertyName:
                        string propertyName = r.Value.ToString();
                        JProperty property = new JProperty(propertyName);
                        property.SetLineInfo(lineInfo);
                        JObject parentObject = (JObject)parent;
                        // handle multiple properties with the same name in JSON
                        JProperty existingPropertyWithName = parentObject.Property(propertyName);
                        if (existingPropertyWithName == null)
                            parent.Add(property);
                        else
                            existingPropertyWithName.Replace(property);
                        parent = property;
                        break;
                    default:
                        throw new InvalidOperationException("The JsonReader should not be on a token of type {0}.".FormatWith(CultureInfo.InvariantCulture, r.TokenType));
                }
            }
            while (r.Read());
        }

        internal int ContentsHashCode()
        {
            int hashCode = 0;
            foreach (JToken item in Children())
            {
                hashCode ^= item.GetDeepHashCode();
            }
            return hashCode;
        }

#if !SILVERLIGHT
        string ITypedList.GetListName(PropertyDescriptor[] listAccessors)
        {
            return string.Empty;
        }

        PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] listAccessors)
        {
            JObject o = First as JObject;
            if (o != null)
            {
                // explicitly use constructor because compact framework has no provider
                JTypeDescriptor descriptor = new JTypeDescriptor(o);
                return descriptor.GetProperties();
            }

            return null;
        }
#endif

        #region IList<JToken> Members

        int IList<JToken>.IndexOf(JToken item)
        {
            return IndexOfItem(item);
        }

        void IList<JToken>.Insert(int index, JToken item)
        {
            InsertItem(index, item);
        }

        void IList<JToken>.RemoveAt(int index)
        {
            RemoveItemAt(index);
        }

        JToken IList<JToken>.this[int index]
        {
            get { return GetItem(index); }
            set { SetItem(index, value); }
        }

        #endregion

        #region ICollection<JToken> Members

        void ICollection<JToken>.Add(JToken item)
        {
            Add(item);
        }

        void ICollection<JToken>.Clear()
        {
            ClearItems();
        }

        bool ICollection<JToken>.Contains(JToken item)
        {
            return ContainsItem(item);
        }

        void ICollection<JToken>.CopyTo(JToken[] array, int arrayIndex)
        {
            CopyItemsTo(array, arrayIndex);
        }

        int ICollection<JToken>.Count
        {
            get { return CountItems(); }
        }

        bool ICollection<JToken>.IsReadOnly
        {
            get { return false; }
        }

        bool ICollection<JToken>.Remove(JToken item)
        {
            return RemoveItem(item);
        }

        #endregion

        private JToken EnsureValue(object value)
        {
            if (value == null)
                return null;

            if (value is JToken)
                return (JToken)value;

            throw new ArgumentException("Argument is not a JToken.");
        }

        #region IList Members

        int IList.Add(object value)
        {
            Add(EnsureValue(value));
            return CountItems() - 1;
        }

        void IList.Clear()
        {
            ClearItems();
        }

        bool IList.Contains(object value)
        {
            return ContainsItem(EnsureValue(value));
        }

        int IList.IndexOf(object value)
        {
            return IndexOfItem(EnsureValue(value));
        }

        void IList.Insert(int index, object value)
        {
            InsertItem(index, EnsureValue(value));
        }

        bool IList.IsFixedSize
        {
            get { return false; }
        }

        bool IList.IsReadOnly
        {
            get { return false; }
        }

        void IList.Remove(object value)
        {
            RemoveItem(EnsureValue(value));
        }

        void IList.RemoveAt(int index)
        {
            RemoveItemAt(index);
        }

        object IList.this[int index]
        {
            get { return GetItem(index); }
            set { SetItem(index, EnsureValue(value)); }
        }

        #endregion

        #region ICollection Members

        void ICollection.CopyTo(Array array, int index)
        {
            CopyItemsTo(array, index);
        }

        int ICollection.Count
        {
            get { return CountItems(); }
        }

        bool ICollection.IsSynchronized
        {
            get { return false; }
        }

        object ICollection.SyncRoot
        {
            get
            {
                if (_syncRoot == null)
                    Interlocked.CompareExchange(ref _syncRoot, new object(), null);

                return _syncRoot;
            }

        }

        #endregion

        #region IBindingList Members

#if !SILVERLIGHT
        void IBindingList.AddIndex(PropertyDescriptor property)
        {
        }

        object IBindingList.AddNew()
        {
            AddingNewEventArgs args = new AddingNewEventArgs();
            OnAddingNew(args);

            if (args.NewObject == null)
                throw new Exception("Could not determine new value to add to '{0}'.".FormatWith(CultureInfo.InvariantCulture, GetType()));

            if (!(args.NewObject is JToken))
                throw new Exception("New item to be added to collection must be compatible with {0}.".FormatWith(CultureInfo.InvariantCulture, typeof(JToken)));

            JToken newItem = (JToken)args.NewObject;
            Add(newItem);

            return newItem;
        }

        bool IBindingList.AllowEdit
        {
            get { return true; }
        }

        bool IBindingList.AllowNew
        {
            get { return true; }
        }

        bool IBindingList.AllowRemove
        {
            get { return true; }
        }

        void IBindingList.ApplySort(PropertyDescriptor property, ListSortDirection direction)
        {
            throw new NotSupportedException();
        }

        int IBindingList.Find(PropertyDescriptor property, object key)
        {
            throw new NotSupportedException();
        }

        bool IBindingList.IsSorted
        {
            get { return false; }
        }

        void IBindingList.RemoveIndex(PropertyDescriptor property)
        {
        }

        void IBindingList.RemoveSort()
        {
            throw new NotSupportedException();
        }

        ListSortDirection IBindingList.SortDirection
        {
            get { return ListSortDirection.Ascending; }
        }

        PropertyDescriptor IBindingList.SortProperty
        {
            get { return null; }
        }

        bool IBindingList.SupportsChangeNotification
        {
            get { return true; }
        }

        bool IBindingList.SupportsSearching
        {
            get { return false; }
        }

        bool IBindingList.SupportsSorting
        {
            get { return false; }
        }
#endif

        #endregion
    }
}